package com.uxsino.commons.loganalyzer.nginx;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.uxsino.commons.loganalyzer.nginx.func.IModifyData;
import com.uxsino.commons.loganalyzer.nginx.utils.ArrayUtils;
import com.uxsino.commons.loganalyzer.nginx.utils.DateUtils;
import com.uxsino.commons.loganalyzer.nginx.utils.FileUtils;

/*
 * 
 * log format '$remote_addr###$remote_user###$time_iso8601'
 * '###$request###$status###$body_bytes_sent###$http_referer###$http_user_agent###$http_x_forwarded_for';
 */
public class NginxLogAnalyzer {

    private static Logger logger = LoggerFactory.getLogger(NginxLogAnalyzer.class);

    private static final int remote_addr_index = 0;

    private static final int time_index = 2;

    private static final int request_index = 3;

    private static final int status_index = 4;

    private static final int bytes_sent_index = 5;

    private static Map<String, Byte> requestTypeMap;

    private static Map<String, Byte> errorTypeMap;

    static {
        requestTypeMap = new HashMap<>();
        requestTypeMap.put("GET", (byte) 0);
        requestTypeMap.put("PUT", (byte) 1);
        requestTypeMap.put("POST", (byte) 2);
        requestTypeMap.put("DELETE", (byte) 3);
        requestTypeMap.put("HEAD", (byte) 4);
        requestTypeMap.put("CONNECT", (byte) 5);
        requestTypeMap.put("OTHER", (byte) 6);

        errorTypeMap = new HashMap<>();
        errorTypeMap.put("4xx", (byte) 0);
        errorTypeMap.put("5xx", (byte) 1);
    }

    private Double[] bytesFlow24hr;    // 24hr

    private Long[][] requests24hr;   // 7 types of requests:GET,PUT, POST, DELETE,HEAD,CONNECT,others

    private Long[][] error24hr;      // 2 types of errors: 4xx, 5xx

    private Long[][] requests7day;   // 7 types of requests

    private JSONArray errorArr1hr; // 24hr error log

    private String[] fileNames;

    private String filePrefixPattern;

    private OffsetDateTime curDateTime;

    private OffsetDateTime day7ago;

    public NginxLogAnalyzer(String filePrefix, String[] fileNamesOri) {
        fileNames = fileNamesOri;
        filePrefixPattern = filePrefix;

        bytesFlow24hr = new Double[24];
        requests24hr = new Long[7][24];
        error24hr = new Long[2][24];
        requests7day = new Long[7][7];
        errorArr1hr = new JSONArray();
        Arrays.fill(bytesFlow24hr, 0.0d);
        ArrayUtils.fillTwoDArray(requests24hr, 0l);
        ArrayUtils.fillTwoDArray(error24hr, 0l);
        ArrayUtils.fillTwoDArray(requests7day, 0l);

        curDateTime = OffsetDateTime.now();
        day7ago = curDateTime.minusDays(6L).truncatedTo(ChronoUnit.DAYS);

    }

    public JSONObject getLogAnalysisReport() {
        for (String fileName : fileNames) {
            doLogAnalyze(fileName);
            FileUtils.deleteFile(filePrefixPattern, fileName);
        }
        JSONObject result = wrapDataToJSONObj();

        return result;
    }

    private void doLogAnalyze(String fileName) {
        BufferedReader in;
        try {
            in = new BufferedReader(new FileReader(filePrefixPattern + fileName));
        } catch (FileNotFoundException e1) {
            logger.error("error opening file {}", filePrefixPattern + fileName, e1);
            return;
        }
        try {
            String line = in.readLine();
            while (line != null) {
                getData(line);
                line = in.readLine();
            }
        } catch (IOException e) {
            logger.error("error reading lines", e);
        }
        try {
            in.close();
        } catch (IOException e) {
            logger.error("fail to close file", e);
        }

    }

    private void getData(String line) {
        String[] infos = line.split("###");
        OffsetDateTime recordTime = OffsetDateTime.parse(infos[time_index]);
        if (recordTime.isBefore(day7ago)) {
            return;
        }
        int i = infos[request_index].indexOf(" ");
        String requestType = "OTHER";
        if (i != -1) {
            requestType = infos[request_index].substring(0, i);
        }
        update7dayRecord(recordTime, requestType);
        update24hrRecord(recordTime, requestType, infos);
    }

    private void update24hrRecord(OffsetDateTime recordTime, String requestType, String[] infos) {
        int hourIndex = DateUtils.getHourIndex(recordTime, curDateTime);

        if (hourIndex >= 0 && hourIndex < 24) {
            requests24hr[requestTypeMap.get(requestType)][hourIndex]++;

            double bytesFlow = Double.parseDouble(infos[bytes_sent_index]);
            if (bytesFlow > 0l) {
                bytesFlow24hr[hourIndex] += bytesFlow;
            }

            int status = Integer.parseInt(infos[status_index]);
            if (status >= 400 && status < 600) {
                int index = (status - 400) / 100;
                error24hr[index][hourIndex]++;

                if (hourIndex == 23) {
                    JSONObject errObj = new JSONObject();
                    errorArr1hr.add(errObj);
                    errObj.put("remoteAddress", infos[remote_addr_index]);
                    errObj.put("time", infos[time_index]);
                    errObj.put("request", infos[request_index]);
                    errObj.put("status", infos[status_index]);
                    errObj.put("bytesSend", infos[bytes_sent_index]);
                }
            }
        }
    }

    private void update7dayRecord(OffsetDateTime recordTime, String requestType) {
        int dayIndex = DateUtils.getDayIndex(recordTime, curDateTime);
        if (dayIndex < 0 || dayIndex > 6) {
            logger.error("record time {} is not within seven days", recordTime.toString());
        } else {
            requests7day[requestTypeMap.get(requestType)][dayIndex]++;
        }
    }

    private JSONObject wrapDataToJSONObj() {
        String[] time24hr = new String[24];
        OffsetDateTime curHour = curDateTime.truncatedTo(ChronoUnit.HOURS);
        for (int i = 0; i < time24hr.length; i++) {
            time24hr[i] = curHour.minusHours(23l - i)
                .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.MEDIUM));
        }
        String[] time7day = new String[7];
        OffsetDateTime curDay = curDateTime.truncatedTo(ChronoUnit.DAYS);
        for (int i = 0; i < time7day.length; i++) {
            time7day[i] = curDay.minusDays(6l - i).format(DateTimeFormatter.ISO_LOCAL_DATE);
        }

        JSONObject result = new JSONObject();

        IModifyData doNothingFunc = a -> a;
        // IModifyData convertToMBFunc = a -> (double) a / 1024.0 / 1024.0;
        result.put("byte_send_24hr", fillJSONArray(time24hr, bytesFlow24hr, doNothingFunc));

        JSONObject request24hrObj = new JSONObject();
        result.put("all_requests_24hr", request24hrObj);
        JSONObject request7dayObj = new JSONObject();
        result.put("all_requests_7day", request7dayObj);
        JSONObject error24hrObj = new JSONObject();
        result.put("all_errors_24hr", error24hrObj);

        for (Entry<String, Byte> entry : requestTypeMap.entrySet()) {
            request24hrObj.put(entry.getKey() + "_request_24hr",
                fillJSONArray(time24hr, requests24hr[entry.getValue()], doNothingFunc));
            request7dayObj.put(entry.getKey() + "_request_7day",
                fillJSONArray(time7day, requests7day[entry.getValue()], doNothingFunc));
        }
        for (Entry<String, Byte> entry : errorTypeMap.entrySet()) {
            error24hrObj.put(entry.getKey() + "_error_24hr",
                fillJSONArray(time24hr, error24hr[entry.getValue()], doNothingFunc));
        }
        result.put("last_1hr_error", errorArr1hr);
        return result;
    }

    private JSONArray fillJSONArray(String[] timeArr, Object[] resArr, IModifyData func) {
        if (timeArr.length != resArr.length) {
            logger.error("time array length != result array length");
            return null;
        }
        JSONArray result = new JSONArray();
        for (int i = 0; i < timeArr.length; i++) {
            JSONObject obj = new JSONObject();
            obj.put(timeArr[i].toString(), func.modifyData(resArr[i]));
            result.add(obj);
        }
        return result;
    }
}
